package de.jmda.mview.typeshape.relation;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import de.jmda.mview.typeshape.RectangleSide;
import de.jmda.mview.typeshape.TypeShape;
import de.jmda.util.gui.awt.graphics.Compass;
import de.jmda.util.gui.awt.graphics.Compass.Direction;
import de.jmda.util.gui.awt.graphics.RelationEndpoint;
import de.jmda.util.gui.awt.graphics.RelationEndpoint.Style;


/**
 * Base class for type shape relations.
 */
public abstract class Relation extends MouseAdapter
{
	/**
	 * injected into constructor
	 */
	private Component containingComponent;
	
	/**
	 * initialised from constructor parameters
	 */
	private RelationEndpointHelper relationEndpointHelperSource;

	/**
	 * initialised from constructor parameters
	 */
	private RelationEndpointHelper relationEndpointHelperTarget;
	
	/**
	 * Connection between relation endpoints represented by {@link
	 * #relationEndpointHelperSource} and {@link #relationEndpointHelperTarget}.
	 */
	private RelationEndpointConnection relationEndpointConnection;
	
	/**
	 * not <code>null</code> if a relation endpoint is currently being dragged
	 */
	private RelationEndpoint relationEndpointBeingDragged;
	
	/**
	 * Initialises and stores general object state. Invokes detailed
	 * initialisation in {@link #initFromTypeShapes()}.
	 *
	 * @param typeShapeSource
	 * @param typeShapeTarget
	 * @param containingComponent component that already does or will contain the
	 *                            type shapes
	 */
	public Relation(
			TypeShape<?> typeShapeSource,
			TypeShape<?> typeShapeTarget,
			Component containingComponent)
  {
		super();

	  this.containingComponent = containingComponent;

		// update bidirectional relation between TypeShapes (via helpers) and this
		// Relation
		relationEndpointHelperSource =
				new RelationEndpointHelper(
						typeShapeSource, getStyleSource(), typeShapeTarget);
	  relationEndpointHelperTarget =
	  		new RelationEndpointHelper(
	  				typeShapeTarget, getStyleTarget(), typeShapeSource);
		relationEndpointConnection = createRelationEndpointConnection();

	  typeShapeSource.addRelation(this);
	  typeShapeTarget.addRelation(this);
	  
	  arrangeRelationEndpoints();
	  updateRelationEndpointConnection();
//	  update(typeShapeSource);
//	  update(typeShapeTarget);

	  containingComponent.addMouseListener(this);
		containingComponent.addMouseMotionListener(this);
  }

	private void arrangeRelationEndpoints()
  {
		arrangeRelationEndpoints(relationEndpointHelperSource);
		arrangeRelationEndpoints(relationEndpointHelperTarget);
  }

	private void arrangeRelationEndpoints(
      RelationEndpointHelper relationEndpointHelper)
  {
		// shortcut, type shape at which relation endpoints are to be arranged
		TypeShape<?> relationEndpointTypeShape =
				relationEndpointHelper.getTypeShape();

		// all relations at the same anchor side as that of relation endpoint helper 
		Set<Relation> relationsAtAnchorSide =
				relationEndpointTypeShape.getRelations(
						relationEndpointHelper.getAnchorSide());
		
		// all relation endpoint helpers that are to be arranged
		List<RelationEndpointHelper> relationEndpointHelpersToArrange =
				new LinkedList<RelationEndpointHelper>();

		// compute all relation endpoint helpers that are to be arranged
		for (Relation relationAtAnchorSide : relationsAtAnchorSide)
		{
			// both, source and target might have to be arranged
			if (relationAtAnchorSide.relationEndpointHelperSource.getTypeShape() ==
			    relationEndpointTypeShape)
			{
				relationEndpointHelpersToArrange.add(
						relationAtAnchorSide.relationEndpointHelperSource);
			}
			if (relationAtAnchorSide.relationEndpointHelperTarget.getTypeShape() ==
			    relationEndpointTypeShape)
			{
				relationEndpointHelpersToArrange.add(
						relationAtAnchorSide.relationEndpointHelperTarget);
			}
		}
		
		Collections.sort(
				relationEndpointHelpersToArrange, COMPARATOR_FOR_ANCHOR_SHIFTING_RATIO);
		
		double length;
		
		if (RectangleSide.isHorizontal(relationEndpointHelper.getAnchorSide()))
		{
			length = relationEndpointHelper.getTypeShape().getBounds().getWidth();
		}
		else
		{
			length = relationEndpointHelper.getTypeShape().getBounds().getHeight();
		}
		
		double space = length / (relationEndpointHelpersToArrange.size() + 1);
		double ratio = space;
		
		for (RelationEndpointHelper relationEndpointHelperToArrange :
		                            relationEndpointHelpersToArrange)
		{
			relationEndpointHelperToArrange.setAnchorShiftingRatio(ratio / length);
			ratio += space;
		}
  }
	
	public abstract Style getStyleSource();
	public abstract Style getStyleTarget();

	/**
	 * @param graphics2d
	 */
	public void paint(Graphics2D graphics2d)
  {
		relationEndpointHelperSource.paint(graphics2d);
  	relationEndpointHelperTarget.paint(graphics2d);
		relationEndpointConnection.paint(graphics2d);
  }

	/**
	 * Controls cursor look (mouse icon).
	 *
   * @see java.awt.event.MouseAdapter#mouseMoved(java.awt.event.MouseEvent)
   */
  @Override
  public void mouseMoved(MouseEvent e)
  {
		super.mouseMoved(e);

		int x = e.getX();
		int y = e.getY();
		
		if (relationEndpointHelperSource.getRelationEndpoint().contains(x, y) ||
				relationEndpointHelperTarget.getRelationEndpoint().contains(x, y))
		{
			containingComponent.setCursor(
					Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
		}
		else
		{
			// TODO: anything else that might be dragged?
			containingComponent.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		}

		e.consume();
  }

	@Override
  public void mousePressed(MouseEvent e)
  {
		int x = e.getX();
		int y = e.getY();

		if (relationEndpointHelperSource.getRelationEndpoint().contains(x, y))
		{
			relationEndpointBeingDragged =
					relationEndpointHelperSource.getRelationEndpoint();

			containingComponent.setCursor(new Cursor(Cursor.MOVE_CURSOR));
		}
		else if (relationEndpointHelperTarget.getRelationEndpoint().contains(x, y))
		{
			relationEndpointBeingDragged =
					relationEndpointHelperTarget.getRelationEndpoint();

			containingComponent.setCursor(new Cursor(Cursor.MOVE_CURSOR));
		}
		else
		{
			// TODO: anything else that might be dragged?
			return;
		}
  }

	@Override
  public void mouseReleased(MouseEvent e)
  {
		relationEndpointBeingDragged = null;
		// TODO: anything else that might be dragged?

		containingComponent.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
  }

  @Override
  public void mouseDragged(MouseEvent e)
  {
  	if (e.isConsumed())
  	{
  		return;
  	}
  	
  	if (relationEndpointBeingDragged == null)
  	{
  		return;
  	}
  	
		if (relationEndpointBeingDragged ==
		    relationEndpointHelperSource.getRelationEndpoint())
		{
			mouseDraggedRelationEndpoint(e, relationEndpointHelperSource);
		}
		else if (relationEndpointBeingDragged ==
		         relationEndpointHelperTarget.getRelationEndpoint())
		{
			mouseDraggedRelationEndpoint(e, relationEndpointHelperTarget);
		}
		else
		{
			// TODO: anything else that might be dragged?
		}

		containingComponent.repaint();
		e.consume();
  }

	/**
   * @param e
	 * @param relationEndpointHelper
   */
  private void mouseDraggedRelationEndpoint(
  		MouseEvent e,
  		RelationEndpointHelper relationEndpointHelper)
  {
	  if ((relationEndpointHelper.getAnchorSide() == RectangleSide.TOP) ||
	      (relationEndpointHelper.getAnchorSide() == RectangleSide.BOTTOM))
	  {
	  	mouseDraggedRelationEndpointHorizontically(
	  			e, relationEndpointHelper);
	  }
	  else // ((anchor.anchorSide == RectangleSide.LEFT) ||
	       //  (anchor.anchorSide == RectangleSide.RIGHT))
	  {
	  	mouseDraggedRelationEndpointVertically(
	  			e, relationEndpointHelper);
	  }
	  
	  updateRelationEndpointConnection();
  }

	/**
	 * @param e
	 * @param relationEndpointHelper
	 */
	private void mouseDraggedRelationEndpointHorizontically(
			MouseEvent e,
			RelationEndpointHelper relationEndpointHelper)
  {
	  Rectangle bounds =
	  		relationEndpointHelper.getTypeShape().getBounds();

	  int x = e.getX();
	  
	  if ((x >= bounds.getMinX()) && (x <= bounds.getMaxX()))
	  {
	  	// mouse position is between left and right type shape bounds
	  	relationEndpointHelper.setAnchorShiftingRatio(
	  			(x - bounds.getMinX()) / bounds.width);
	  }
	  else
	  {
	  	// vertex of rectangle (bounds), direction source point
  	  Point vertex;
  	  
	  	if (x < bounds.getMinX())
	  	{
	  		// mouse is left from the type shape bounds
	  		if (relationEndpointHelper.getAnchorSide() == RectangleSide.TOP)
	  		{
	  			vertex = new Point((int) bounds.getMinX(), (int) bounds.getMinY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.NORTH)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(0);
	  			}
	  			else if (direction == Direction.WEST)
	  			{
	  				// change anchor side to left and set shifting ratio to 0
	  				relationEndpointHelper.setAnchor(RectangleSide.LEFT, 0);
	  			}
	  		}
	  		else // anchor.anchorSide == RectangleSide.BOTTOM
	  		{
	  			vertex = new Point((int) bounds.getMinX(), (int) bounds.getMaxY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.SOUTH)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(0);
	  			}
	  			else if (direction == Direction.WEST)
	  			{
	  				// change anchor side to left and set shifting ratio to 1
	  				relationEndpointHelper.setAnchor(RectangleSide.LEFT, 1);
	  			}
	  		}
	  	}
	  	else // x > bounds.getMaxX()
	  	{
	  		if (relationEndpointHelper.getAnchorSide() == RectangleSide.TOP)
	  		{
	  			vertex = new Point((int) bounds.getMaxX(), (int) bounds.getMinY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.NORTH)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(1);
	  			}
	  			else if (direction == Direction.EAST)
	  			{
	  				// change anchor side to right and set shifting ratio to 0
	  				relationEndpointHelper.setAnchor(RectangleSide.RIGHT, 0);
	  			}
	  		}
	  		else // anchor.anchorSide == RectangleSide.BOTTOM
	  		{
	  			vertex = new Point((int) bounds.getMaxX(), (int) bounds.getMaxY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.SOUTH)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(1);
	  			}
	  			else if (direction == Direction.EAST)
	  			{
	  				// change anchor side to left and set shifting ratio to 1
	  				relationEndpointHelper.setAnchor(RectangleSide.RIGHT, 1);
	  			}
	  		}
	  	}
	  }
  }

	private void mouseDraggedRelationEndpointVertically(
			MouseEvent e,
			RelationEndpointHelper relationEndpointHelper)
  {
	  Rectangle bounds = relationEndpointHelper.getTypeShape().getBounds();

	  int y = e.getY();
	  
	  if ((y >= bounds.getMinY()) && (y <= bounds.getMaxY()))
	  {
	  	// mouse position is between upper and lower type shape bounds
	  	relationEndpointHelper.setAnchorShiftingRatio(
	  			(y - bounds.getMinY()) / bounds.height);
	  }
	  else
	  {
	  	// vertex of rectangle (bounds), direction source point
		  Point vertex;
		  
	  	if (y < bounds.getMinY())
	  	{
	  		// mouse is above the type shape bounds
	  		if (relationEndpointHelper.getAnchorSide() == RectangleSide.LEFT)
	  		{
	  			vertex = new Point((int) bounds.getMinX(), (int) bounds.getMinY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.WEST)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(0);
	  			}
	  			else if (direction == Direction.NORTH)
	  			{
	  				// change anchor side to top and set shifting ratio to 0
	  				relationEndpointHelper.setAnchor(RectangleSide.TOP, 0);
	  			}
	  		}
	  		else // anchor.anchorSide == RectangleSide.RIGHT
	  		{
	  			vertex = new Point((int) bounds.getMaxX(), (int) bounds.getMinY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.EAST)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(0);
	  			}
	  			else if (direction == Direction.NORTH)
	  			{
	  				// change anchor side to top and set shifting ratio to 1
	  				relationEndpointHelper.setAnchor(RectangleSide.TOP, 1);
	  			}
	  		}
	  	}
	  	else // y > bounds.getMaxY()
	  	{
	  		if (relationEndpointHelper.getAnchorSide() == RectangleSide.LEFT)
	  		{
	  			vertex = new Point((int) bounds.getMinX(), (int) bounds.getMaxY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.WEST)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(1);
	  			}
	  			else if (direction == Direction.SOUTH)
	  			{
	  				// change anchor side to bottom and set shifting ratio to 0
	  				relationEndpointHelper.setAnchor(RectangleSide.BOTTOM, 0);
	  			}
	  		}
	  		else // anchor.anchorSide == RectangleSide.RIGHT
	  		{
	  			vertex = new Point((int) bounds.getMaxX(), (int) bounds.getMaxY());
	  			
	  			Direction direction = Compass.getDirection(vertex, e.getPoint());
	  			
	  			if (direction == Direction.WEST)
	  			{
	  				relationEndpointHelper.setAnchorShiftingRatio(1);
	  			}
	  			else if (direction == Direction.SOUTH)
	  			{
	  				// change anchor side to bottom and set shifting ratio to 1
	  				relationEndpointHelper.setAnchor(RectangleSide.BOTTOM, 1);
	  			}
	  		}
	  	}
	  }
  }
	
	public void update(TypeShape<?> typeShape)
	{
		if (relationEndpointHelperSource.getTypeShape() == typeShape)
		{
			relationEndpointHelperSource.synchronize();
		}
		
		if (relationEndpointHelperTarget.getTypeShape() == typeShape)
		{
			relationEndpointHelperTarget.synchronize();
		}
		
	  updateRelationEndpointConnection();
	}
	
	public RelationEndpoint getRelationEndpoint(TypeShape<?> typeShape)
	{
		if (relationEndpointHelperSource.getTypeShape() == typeShape)
		{
			return relationEndpointHelperSource.getRelationEndpoint();
		}
		
		if (relationEndpointHelperTarget.getTypeShape() == typeShape)
		{
			return relationEndpointHelperTarget.getRelationEndpoint();
		}
		
		throw new IllegalArgumentException("type shape not part of this relation");
	}

	private void updateRelationEndpointConnection()
  {
		try
		{
			relationEndpointConnection.update();
		}
		catch (StateInvalidatedException e)
		{
			relationEndpointHelperSource.setLength(
					RelationEndpointHelper.INITIAL_RELATION_ENDPOINT_LENGTH);
			relationEndpointHelperTarget.setLength(
					RelationEndpointHelper.INITIAL_RELATION_ENDPOINT_LENGTH);
			relationEndpointConnection = createRelationEndpointConnection();
		}
  }

	private RelationEndpointConnection createRelationEndpointConnection()
  {
		RelationEndpointConnection result = null;

		try
		{
			result =
					new RECOH2HFacing(
							relationEndpointHelperSource, relationEndpointHelperTarget);
		}
		catch (IllegalArgumentException e)
		{
			// intentionally left blank
		}
		
		if (result != null)
		{
			return result;
		}
		
		try
		{
			result =
					new RECOH2HNonFacing(
							relationEndpointHelperSource, relationEndpointHelperTarget);
		}
		catch (IllegalArgumentException e)
		{
			// intentionally left blank
		}
		
		if (result != null)
		{
			return result;
		}
		
		try
		{
			result =
					new RECOV2VFacing(
							relationEndpointHelperSource, relationEndpointHelperTarget);
		}
		catch (IllegalArgumentException e1)
		{
			// intentionally left blank
		}
		
		if (result != null)
		{
			return result;
		}
		
		try
		{
			result =
					new RECOV2VNonFacing(
							relationEndpointHelperSource, relationEndpointHelperTarget);
		}
		catch (IllegalArgumentException e1)
		{
			// intentionally left blank
		}
		
		if (result != null)
		{
			return result;
		}
		
		try
		{
			result =
					new RECOH2V(
							relationEndpointHelperSource, relationEndpointHelperTarget);
		}
		catch (IllegalArgumentException e1)
		{
			// intentionally left blank
		}
		
		if (result != null)
		{
			return result;
		}
		
		throw new IllegalArgumentException(
				"failure creating relation endpoint connection");
  }

	private final static Comparator<RelationEndpointHelper>
			COMPARATOR_FOR_ANCHOR_SHIFTING_RATIO =
					new Comparator<RelationEndpointHelper>()
					{
						@Override
				    public int compare(
				    		RelationEndpointHelper o1, RelationEndpointHelper o2)
				    {
				      return
				      		(new Double(o1.getAnchorShiftingRatio())).compareTo(
				      		 new Double(o2.getAnchorShiftingRatio()));
				    }
					};
}